/*
  Copyright 2012 Sergey Ostanin

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
*/

package ru.irenproject.it;

import static ru.irenproject.it.XmlUtils.find;
import static ru.irenproject.it.XmlUtils.findBoolean;
import static ru.irenproject.it.XmlUtils.findChild;
import static ru.irenproject.it.XmlUtils.findInteger;
import static ru.irenproject.it.XmlUtils.getChild;
import static ru.irenproject.it.XmlUtils.getChildren;
import static ru.irenproject.it.XmlUtils.xmlValueToInteger;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;

import javax.inject.Inject;

import org.apache.commons.io.output.ByteArrayOutputStream;
import org.w3c.dom.Element;

import ru.irenproject.MarkScale;
import ru.irenproject.Modifier;
import ru.irenproject.ModifierList;
import ru.irenproject.Profile;
import ru.irenproject.ProfileList;
import ru.irenproject.Question;
import ru.irenproject.QuestionItem;
import ru.irenproject.Section;
import ru.irenproject.SectionTree;
import ru.irenproject.Test;
import ru.irenproject.TestProtos;
import ru.irenproject.TestProtos.AvailableQuestionScore;
import ru.irenproject.TestProtos.AvailableScore;
import ru.irenproject.TestProtos.AvailableSectionScore;
import ru.irenproject.pad.Image;
import ru.irenproject.pad.ImageSource;
import ru.irenproject.pad.Pad;
import ru.irenproject.pad.PadProtos.ImageFormat;
import ru.irenproject.store.Store;

import com.google.inject.assistedinject.Assisted;
import com.google.protobuf.ByteString;

public final class ItReader {
  private final Map<String, QuestionReader> fQuestionReaders;
  private final Map<String, ModifierReader> fModifierReaders;
  private final ZipFile fZip;
  private final Element fRoot;
  private final Store fStore;
  private final Map<ItImage, Image> fImageCache = new HashMap<>();

  @Inject ItReader(
      Map<String, QuestionReader> questionReaders,
      Map<String, ModifierReader> modifierReaders,
      @Assisted ZipFile zip, @Assisted Element root, @Assisted Store store) {
    fQuestionReaders = questionReaders;
    fModifierReaders = modifierReaders;
    fZip = zip;
    fRoot = root;
    fStore = store;
  }

  public Element root() {
    return fRoot;
  }

  public Test readTest(Element e) {
    Test res = fStore.create(Test.class);
    readSection(findChild(e, It.ELEM_SECTION), res.sectionTree().rootSection(),
        res.sectionTree());

    Element xmlProfiles = getChild(e, It.ELEM_PROFILES);
    if (xmlProfiles != null) {
      readProfiles(xmlProfiles, res.profileList());
    }

    return res;
  }

  private void readSection(Element e, Section out, SectionTree sectionTree) {
    out.setName(find(e, It.ATTR_TITLE));

    Element xmlQuestions = getChild(e, It.ELEM_QUESTIONS);
    if (xmlQuestions != null) {
      readQuestionList(xmlQuestions, out);
    }

    Element xmlSections = getChild(e, It.ELEM_SECTIONS);
    if (xmlSections != null) {
      readSections(xmlSections, out, sectionTree);
    }

    Element xmlModifiers = getChild(e, It.ELEM_MODIFIERS);
    if (xmlModifiers != null) {
      readModifiers(xmlModifiers, out.modifierList());
    }
  }

  private void readQuestionList(Element e, Section out) {
    for (Element xmlQuestion : getChildren(e, It.ELEM_QUESTION)) {
      out.questionList().addItem(readQuestionItem(xmlQuestion));
    }
  }

  private void readSections(Element e, Section out, SectionTree sectionTree) {
    for (Element xmlSection : getChildren(e, It.ELEM_SECTION)) {
      Section section = fStore.create(Section.class);
      sectionTree.add(section, out);
      readSection(xmlSection, section, sectionTree);
    }
  }

  public QuestionItem readQuestionItem(Element e) {
    QuestionReader reader = fQuestionReaders.get(find(e, It.ATTR_TYPE));
    ItCheck.input(reader != null);
    Question q = reader.read(this, e, fStore);

    QuestionItem res = QuestionItem.create(q);
    res.setWeight(findInteger(e, It.ATTR_WEIGHT));
    res.setEnabled(findBoolean(e, It.ATTR_ENABLED));

    Element xmlLabels = getChild(e, It.ELEM_LABELS);
    if (xmlLabels != null) {
      for (Element xmlLabel : getChildren(xmlLabels, It.ELEM_LABEL)) {
        res.addLabel(find(xmlLabel, It.ATTR_VALUE));
      }
    }

    Element xmlModifiers = getChild(e, It.ELEM_MODIFIERS);
    if (xmlModifiers != null) {
      readModifiers(xmlModifiers, res.modifierList());
    }

    return res;
  }

  public void readPad(Element e, Pad out) {
    out.clear();

    for (Element child : getChildren(e, null)) {
      switch (child.getTagName()) {
        case It.ELEM_TEXT: {
          readText(child, out);
          break;
        }
        case It.ELEM_IMG: {
          readImage(child, out);
          break;
        }
        case It.ELEM_BR: {
          readLineBreak(child, out);
          break;
        }
        default: {
          throw new ItInputException();
        }
      }
    }
  }

  private void readText(Element e, Pad out) {
    out.appendText(find(e, It.ATTR_VALUE));
  }

  private void readImage(Element e, Pad out) {
    String src = find(e, It.ATTR_SRC);
    String clipboardFormat = e.hasAttribute(It.ATTR_CLIPBOARD_FORMAT) ?
        find(e, It.ATTR_CLIPBOARD_FORMAT) : null;
    String clipboardData = (clipboardFormat == null) ?
        null : find(e, It.ATTR_CLIPBOARD_DATA);
    ItImage itImage = new ItImage(src, clipboardFormat, clipboardData);

    Image image = fImageCache.get(itImage);
    if (image == null) {
      ImageFormat format;
      if (src.endsWith("." + It.IT_IMAGE_PNG_EXTENSION)) {
        format = ImageFormat.PNG;
      } else if (src.endsWith("." + It.IT_IMAGE_JPG_EXTENSION)) {
        format = ImageFormat.JPEG;
      } else {
        throw new ItInputException();
      }

      ByteString data = readZipEntry(fZip, src);

      ImageSource source = (clipboardFormat == null) ?
          null : ImageSource.create(clipboardFormat, readZipEntry(fZip, clipboardData));

      image = Image.obtain(fStore, format, data, source);
      fImageCache.put(itImage, image);
    }

    out.appendImage(image);
  }

  private static ByteString readZipEntry(ZipFile f, String entryName) {
    try {
      ZipEntry e = f.getEntry(entryName);
      ItCheck.input(e != null);
      ByteArrayOutputStream out = new ByteArrayOutputStream();
      try (InputStream in = f.getInputStream(e)) {
        out.write(in);
      }
      return ByteString.copyFrom(out.toByteArray());
    } catch (ZipException ex) {
      throw new ItInputException(ex);
    } catch (IOException ex) {
      throw new RuntimeException(ex);
    }
  }

  private void readLineBreak(Element e, Pad out) {
    out.appendLineFeed();
  }

  public void readModifiers(Element e, ModifierList out) {
    List<Modifier> list = out.list();
    int i = 0;

    for (Element xmlModifier : getChildren(e, It.ELEM_MODIFIER)) {
      ModifierReader reader = fModifierReaders.get(find(xmlModifier, It.ATTR_TYPE));
      ItCheck.input(reader != null);

      if ((i < list.size()) && (list.get(i).getClass() == reader.modifierClass())) {
        reader.read(this, xmlModifier, list.get(i));
      } else {
        Modifier m = fStore.create(reader.modifierClass());
        reader.read(this, xmlModifier, m);
        if (i < list.size()) {
          list.set(i, m);
        } else {
          list.add(m);
        }
      }

      ++i;
    }

    list.subList(i, list.size()).clear();
    out.setList(list);
  }

  private void readProfiles(Element e, ProfileList out) {
    for (Element xmlProfile : getChildren(e, It.ELEM_PROFILE)) {
      Profile profile = fStore.create(Profile.class);
      readProfile(xmlProfile, profile);
      out.add(profile);
    }
  }

  public void readProfile(Element e, Profile out) {
    out.setTitle(find(e, It.ATTR_TITLE));

    Element xmlQuestionSelection = findChild(e, It.ELEM_QUESTION_SELECTION);
    String questionsPerSection = find(xmlQuestionSelection, It.ATTR_QUESTIONS_PER_SECTION);
    if (questionsPerSection.equals(It.VAL_ALL)) {
      out.clearQuestionsPerSection();
    } else {
      out.setQuestionsPerSection(xmlValueToInteger(questionsPerSection));
    }

    if (xmlQuestionSelection.hasAttribute(It.ATTR_LABEL_FILTER)) {
      out.setLabelFilter(find(xmlQuestionSelection, It.ATTR_LABEL_FILTER));
    } else {
      out.clearLabelFilter();
    }
    out.setShuffleQuestions(findBoolean(xmlQuestionSelection, It.ATTR_SHUFFLE_QUESTIONS));

    Element xmlSessionOptions = findChild(e, It.ELEM_SESSION_OPTIONS);
    String durationMinutes = find(xmlSessionOptions, It.ATTR_DURATION_MINUTES);
    if (durationMinutes.equals(It.VAL_UNLIMITED)) {
      out.clearDurationMinutes();
    } else {
      out.setDurationMinutes(xmlValueToInteger(durationMinutes));
    }

    out.setEditableAnswers(findBoolean(xmlSessionOptions, It.ATTR_EDITABLE_ANSWERS));
    out.setBrowsableQuestions(findBoolean(xmlSessionOptions, It.ATTR_BROWSABLE_QUESTIONS));
    out.setWeightCues(findBoolean(xmlSessionOptions, It.ATTR_WEIGHT_CUES));

    Element xmlInstantFeedback = getChild(e, It.ELEM_INSTANT_FEEDBACK);
    ItCheck.input(out.editableAnswers() == (xmlInstantFeedback == null));
    if (xmlInstantFeedback == null) {
      out.setInstantAnswerCorrectness(false);
      out.setInstantTotalPercentCorrect(false);
    } else {
      out.setInstantAnswerCorrectness(findBoolean(xmlInstantFeedback, It.ATTR_ANSWER_CORRECTNESS));
      out.setInstantTotalPercentCorrect(findBoolean(xmlInstantFeedback, It.ATTR_TOTAL_PERCENT_CORRECT));
    }

    Element xmlTestResults = getChild(e, It.ELEM_TEST_RESULTS);
    if (xmlTestResults == null) {
      out.clearAvailableScore();
    } else {
      out.setAvailableScore(readAvailableScore(xmlTestResults));
    }

    Element xmlMarkScale = getChild(e, It.ELEM_MARK_SCALE);
    out.setMarkScale((xmlMarkScale == null) ? MarkScale.EMPTY : readMarkScale(xmlMarkScale));

    Element xmlModifiers = getChild(e, It.ELEM_MODIFIERS);
    if (xmlModifiers == null) {
      out.modifierList().clear();
    } else {
      readModifiers(xmlModifiers, out.modifierList());
    }
  }

  private AvailableScore readAvailableScore(Element e) {
    AvailableScore.Builder b = AvailableScore.newBuilder();

    b.setPercentCorrect(findBoolean(e, It.ATTR_PERCENT_CORRECT))
     .setPoints(findBoolean(e, It.ATTR_POINTS))
     .setMark(findBoolean(e, It.ATTR_MARK));

    Element xmlQuestionResults = getChild(e, It.ELEM_QUESTION_RESULTS);
    if (xmlQuestionResults != null) {
      b.setForQuestions(readAvailableQuestionScore(xmlQuestionResults));
    }

    Element xmlSectionResults = getChild(e, It.ELEM_SECTION_RESULTS);
    if (xmlSectionResults != null) {
      b.setForSections(readAvailableSectionScore(xmlSectionResults));

      if (b.getForSections().getQuestionList()) {
        ItCheck.input(b.hasForQuestions());
      }
    }

    return b.build();
  }

  private AvailableQuestionScore readAvailableQuestionScore(Element e) {
    return AvailableQuestionScore.newBuilder()
        .setPercentCorrect(findBoolean(e, It.ATTR_PERCENT_CORRECT))
        .setPoints(findBoolean(e, It.ATTR_POINTS))
        .setCorrectAnswer(findBoolean(e, It.ATTR_CORRECT_ANSWER))
        .build();
  }

  private AvailableSectionScore readAvailableSectionScore(Element e) {
    return AvailableSectionScore.newBuilder()
        .setPercentCorrect(findBoolean(e, It.ATTR_PERCENT_CORRECT))
        .setPoints(findBoolean(e, It.ATTR_POINTS))
        .setQuestionCount(findBoolean(e, It.ATTR_QUESTION_COUNT))
        .setQuestionList(findBoolean(e, It.ATTR_QUESTION_LIST))
        .build();
  }

  private MarkScale readMarkScale(Element e) {
    TestProtos.MarkScale.Builder b = TestProtos.MarkScale.newBuilder();
    for (Element xmlMark : getChildren(findChild(e, It.ELEM_MARKS), It.ELEM_MARK)) {
      b.addMarkBuilder()
          .setTitle(find(xmlMark, It.ATTR_TITLE))
          .setLowerBound(find(xmlMark, It.ATTR_LOWER_BOUND));
    }
    return new MarkScale(b.build());
  }
}
